package com.github.auties00.cobalt.model.info;

import com.alibaba.fastjson2.JSONObject;
import com.github.auties00.cobalt.model.contact.Contact;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.model.message.model.MessageContainer;
import com.github.auties00.cobalt.model.message.model.MessageReceipt;
import com.github.auties00.cobalt.model.message.model.MessageStatus;
import com.github.auties00.cobalt.model.newsletter.Newsletter;
import com.github.auties00.cobalt.model.newsletter.NewsletterReaction;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;

import java.time.ZonedDateTime;
import java.util.*;

// TODO: Add receipts
@ProtobufMessage
public final class NewsletterMessageInfo implements MessageInfo {
    @ProtobufProperty(index = 1, type = ProtobufType.STRING)
    final String id;

    @ProtobufProperty(index = 2, type = ProtobufType.INT32)
    final int serverId;

    @ProtobufProperty(index = 3, type = ProtobufType.UINT64)
    final Long timestampSeconds;

    @ProtobufProperty(index = 4, type = ProtobufType.UINT64)
    final Long views;

    @ProtobufProperty(index = 5, type = ProtobufType.MAP, mapKeyType = ProtobufType.STRING, mapValueType = ProtobufType.MESSAGE)
    final Map<String, NewsletterReaction> reactions;

    @ProtobufProperty(index = 6, type = ProtobufType.MESSAGE)
    MessageContainer message;

    @ProtobufProperty(index = 7, type = ProtobufType.ENUM)
    MessageStatus status;

    @ProtobufProperty(index = 8, type = ProtobufType.BOOL)
    boolean starred;

    @ProtobufProperty(index = 9, type = ProtobufType.MESSAGE)
    final MessageReceipt receipt;

    Newsletter newsletter;

    NewsletterMessageInfo(String id, int serverId, Long timestampSeconds, Long views, Map<String, NewsletterReaction> reactions, MessageContainer message, MessageStatus status, boolean starred, MessageReceipt receipt) {
        this.id = Objects.requireNonNull(id, "id cannot be null");
        this.serverId = serverId;
        this.timestampSeconds = timestampSeconds;
        this.views = views;
        this.reactions = reactions;
        this.message = message;
        this.status = status;
        this.starred = starred;
        this.receipt = receipt;
    }

    public static Optional<NewsletterMessageInfo> ofJson(JSONObject jsonObject) {
        if(jsonObject == null) {
            return Optional.empty();
        }

        var id = jsonObject.getString("id");
        if(id == null) {
            return Optional.empty();
        }

        var serverId = jsonObject.getIntValue("serverId", -1);
        var timestampSeconds = jsonObject.getLongValue("timestampSeconds", 0);
        var views = jsonObject.getLongValue("views", 0);
        var reactionsJsonObject = jsonObject.getJSONObject("reactions");
        Map<String, NewsletterReaction> reactions = HashMap.newHashMap(reactionsJsonObject.size());
        for(var reactionKey : reactionsJsonObject.sequencedKeySet()) {
            var reactionJsonObject = reactionsJsonObject.getJSONObject(reactionKey);
            NewsletterReaction.ofJson(reactionJsonObject)
                    .ifPresent(reaction -> reactions.put(reactionKey, reaction));
        }
        var message = MessageContainer.ofJson(jsonObject.getJSONObject("message"))
                .orElse(MessageContainer.empty());
        var status = MessageStatus.of(jsonObject.getString("status"))
                .orElse(MessageStatus.ERROR);
        // TODO
        var starred = false;
        var receipt = new MessageReceipt();
        return Optional.of(new NewsletterMessageInfo(id, serverId, timestampSeconds, views, reactions, message, status, starred, receipt));
    }

    public void setNewsletter(Newsletter newsletter) {
        Objects.requireNonNull(newsletter, "Newsletter cannot be null");
        this.newsletter = newsletter;
    }

    public Jid newsletterJid() {
        Objects.requireNonNull(newsletter, "Newsletter cannot be null");
        return newsletter.jid();
    }

    @Override
    public Jid parentJid() {
        return newsletterJid();
    }

    @Override
    public Optional<MessageInfoParent> parent() {
        return Optional.ofNullable(newsletter);
    }

    @Override
    public void setParent(MessageInfoParent parent) {
        if(parent == null) {
            this.newsletter = null;
        }else if(!(parent instanceof Newsletter parentNewsletter)) {
            throw new IllegalArgumentException("Parent is not a newsletter");
        }else {
            this.newsletter = parentNewsletter;
        }
    }

    @Override
    public Optional<Contact> sender() {
        return Optional.empty();
    }

    @Override
    public void setSender(Contact sender) {

    }

    @Override
    public Jid senderJid() {
        return newsletterJid();
    }

    public Newsletter newsletter() {
        return Objects.requireNonNull(newsletter, "newsletter cannot be null when accessed");
    }

    public String id() {
        return id;
    }

    public int serverId() {
        return serverId;
    }

    @Override
    public OptionalLong timestampSeconds() {
        return timestampSeconds == null ? OptionalLong.empty() : OptionalLong.of(timestampSeconds);
    }

    public OptionalLong views() {
        return views == null ? OptionalLong.empty() : OptionalLong.of(views);
    }

    public MessageContainer message() {
        return message;
    }

    @Override
    public void setMessage(MessageContainer message) {
        this.message = message;
    }

    public Optional<ZonedDateTime> timestamp() {
        return Clock.parseSeconds(timestampSeconds);
    }

    @Override
    public MessageStatus status() {
        return status;
    }

    @Override
    public void setStatus(MessageStatus status) {
        this.status = status;
    }

    public Collection<NewsletterReaction> reactions() {
        return Collections.unmodifiableCollection(reactions.values());
    }

    public Optional<NewsletterReaction> findReaction(String value) {
        return Optional.ofNullable(reactions.get(value));
    }

    public Optional<NewsletterReaction> addReaction(NewsletterReaction reaction) {
        return Optional.ofNullable(reactions.put(reaction.content(), reaction));
    }

    public Optional<NewsletterReaction> removeReaction(String code) {
        return Optional.ofNullable(reactions.remove(code));
    }

    public void incrementReaction(String code, boolean fromMe) {
        findReaction(code).ifPresentOrElse(reaction -> {
            reaction.setCount(reaction.count() + 1);
            reaction.setFromMe(fromMe);
        }, () -> {
            var reaction = new NewsletterReaction(code, 1, fromMe);
            addReaction(reaction);
        });
    }

    public void decrementReaction(String code) {
        findReaction(code).ifPresent(reaction -> {
            if (reaction.count() <= 1) {
                removeReaction(reaction.content());
                return;
            }

            reaction.setCount(reaction.count() - 1);
            reaction.setFromMe(false);
        });
    }

    public boolean starred() {
        return starred;
    }

    public void setStarred(boolean starred) {
        this.starred = starred;
    }

    public MessageReceipt receipt() {
        return receipt;
    }

    @Override
    public String toString() {
        return "NewsletterMessageInfo{" +
               "newsletter=" + newsletter +
               ", id='" + id + '\'' +
               ", serverId=" + serverId +
               ", timestampSeconds=" + timestampSeconds +
               ", views=" + views +
               ", reactions=" + reactions +
               ", message=" + message +
               ", status=" + status +
               '}';
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof NewsletterMessageInfo that &&
               serverId == that.serverId &&
               Objects.equals(id, that.id) &&
               Objects.equals(timestampSeconds, that.timestampSeconds) &&
               Objects.equals(views, that.views) &&
               Objects.equals(reactions, that.reactions) &&
               Objects.equals(message, that.message) &&
               Objects.equals(newsletter, that.newsletter) &&
               status == that.status;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, serverId, timestampSeconds, views, reactions, message, newsletter, status);
    }
}
